package org.anderground.app.storage.db;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.anderground.app.storage.db.DbStorage.CitiesTable;
import org.anderground.app.storage.db.DbStorage.LinesTable;
import org.anderground.app.storage.db.DbStorage.MapsTable;
import org.anderground.app.storage.db.DbStorage.StationsTable;
import org.anderground.app.storage.db.DbStorage.TransfersTable;
import org.anderground.app.storage.db.DbStorage.TunnelsTable;
import org.anderground.core.model.Line;
import org.anderground.core.model.ModelData;
import org.anderground.core.model.Station;
import org.anderground.core.model.Transfer;
import org.anderground.core.model.Tunnel;
import org.anderground.core.model.Station.Label;
import org.anderground.core.storage.ModelWriter;
import org.anderground.core.storage.StorageException;
import org.anderground.core.util.Coord;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.DatabaseUtils.InsertHelper;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

/**
 * @author Eldar Abusalimov
 */
public class DbModelWriter extends AbstractDbWriter<ModelData> implements
		ModelWriter {

	private final long cityId;

	/**
	 * @param db
	 * @param cityId
	 */
	public DbModelWriter(SQLiteDatabase db, long cityId) {
		super(db);
		this.cityId = cityId;
	}

	/**
	 * @param openHelper
	 * @param cityId
	 */
	public DbModelWriter(SQLiteOpenHelper openHelper, long cityId) {
		super(openHelper);
		this.cityId = cityId;
	}

	@Override
	protected void writeToOutput(ModelData data, SQLiteDatabase db)
			throws IOException {

		long mapRowId = insertMap(db, cityId);
		if (mapRowId < 0) {
			throw new StorageException(
					"unable to insert the map row or there is no city row with id "
							+ cityId);
		}

		Map<Station, Long> stationsRowsMap = insertStations(db, data
				.getStations(), mapRowId);
		if (stationsRowsMap == null) {
			throw new StorageException("unable to insert the stations");
		}

		Map<Line, Long> linesRowsMap = insertLines(db, data.getLines(),
				mapRowId);
		if (linesRowsMap == null) {
			throw new StorageException("unable to insert the lines");
		}

		if (!insertTransfers(db, data.getTransfers(), stationsRowsMap)) {
			throw new StorageException("unable to insert the transfers");
		}

		if (!insertTunnels(db, data.getTunnels(), stationsRowsMap, linesRowsMap)) {
			throw new StorageException("unable to insert the tunnels");
		}

	}

	/**
	 * Inserts a single row of the {@link MapsTable} into the given
	 * {@link SQLiteDatabase} and updates the corresponding row in the
	 * {@link CitiesTable}.
	 * 
	 * @param db
	 * @param cityId
	 * @return the rowId of the newly inserted row, or -1 if an error occurred
	 */
	private static long insertMap(SQLiteDatabase db, long cityId) {
		Cursor cursor = db.query(CitiesTable.NAME,
				new String[] { CitiesTable.Columns.MAP_ID },
				CitiesTable.Columns._ID + " = " + cityId, null, null, null,
				null);
		try {
			int tCities_MapId_Index = cursor
					.getColumnIndexOrThrow(CitiesTable.Columns.MAP_ID);
			if (cursor.moveToFirst()) {
				long oldMapRowId = cursor.getLong(tCities_MapId_Index);
				if (oldMapRowId > 0) {
					db.delete(MapsTable.NAME, MapsTable.Columns._ID + " = "
							+ oldMapRowId, null);
				}
			} else {
				return -1;
			}
		} finally {
			cursor.close();
		}
		ContentValues mapValues = new ContentValues();
		mapValues.put(MapsTable.Columns.LAST_MODIFIED, System
				.currentTimeMillis());
		long mapRowId = db.insert(MapsTable.NAME,
				MapsTable.Columns.LAST_MODIFIED, mapValues);
		if (mapRowId < 0) {
			return -1;
		}
		ContentValues cityValues = new ContentValues();
		cityValues.put(CitiesTable.Columns.MAP_ID, mapRowId);
		int rows = db.update(CitiesTable.NAME, cityValues,
				CitiesTable.Columns._ID + " = " + cityId, null);
		if (rows < 1) {
			return -1;
		}
		return mapRowId;
	}

	/**
	 * Inserts the {@link Station}s data into the given {@link SQLiteDatabase}.
	 * 
	 * @param db
	 * @param stations
	 * @param mapRowId
	 * @return the station-to-rowId map of the newly inserted rows, or null if
	 *         an error occurred
	 */
	private static Map<Station, Long> insertStations(SQLiteDatabase db,
			Set<Station> stations, long mapRowId) {

		InsertHelper ih = new InsertHelper(db, StationsTable.NAME);
		final int tStations_Name_Col = ih
				.getColumnIndex(StationsTable.Columns.NAME);
		final int tStations_MapId_Col = ih
				.getColumnIndex(StationsTable.Columns.MAP_ID);
		final int tStations_Open_Col = ih
				.getColumnIndex(StationsTable.Columns.OPEN);
		final int tStations_Close_Col = ih
				.getColumnIndex(StationsTable.Columns.CLOSE);
		final int tStations_Latitude_Col = ih
				.getColumnIndex(StationsTable.Columns.LATITUDE);
		final int tStations_Longitude_Col = ih
				.getColumnIndex(StationsTable.Columns.LONGITUDE);
		final int tStations_PositionX_Col = ih
				.getColumnIndex(StationsTable.Columns.POSITION_X);
		final int tStations_PositionY_Col = ih
				.getColumnIndex(StationsTable.Columns.POSITION_Y);
		final int tStations_LabelDx_Col = ih
				.getColumnIndex(StationsTable.Columns.LABEL_DX);
		final int tStations_LabelDy_Col = ih
				.getColumnIndex(StationsTable.Columns.LABEL_DY);
		final int tStations_LabelAngle_Col = ih
				.getColumnIndex(StationsTable.Columns.LABEL_ANGLE);
		final int tStations_LabelVisible_Col = ih
				.getColumnIndex(StationsTable.Columns.LABEL_VISIBLE);

		Map<Station, Long> stationsRowsMap = new HashMap<Station, Long>(
				stations.size(), 1);

		for (Station station : stations) {

			ih.prepareForInsert();
			Coord location = station.getLocation();
			Coord position = station.getPosition();
			Label label = station.getLabel();

			ih.bind(tStations_Name_Col, station.getName());
			ih.bind(tStations_MapId_Col, mapRowId);
			ih.bind(tStations_Open_Col, station.getOpenTime().toSeconds());
			ih.bind(tStations_Close_Col, station.getCloseTime().toSeconds());
			ih.bind(tStations_Latitude_Col, location.getX());
			ih.bind(tStations_Longitude_Col, location.getY());
			ih.bind(tStations_PositionX_Col, position.getX());
			ih.bind(tStations_PositionY_Col, position.getY());
			ih.bind(tStations_LabelDx_Col, label.getOffset().getX());
			ih.bind(tStations_LabelDy_Col, label.getOffset().getY());
			ih.bind(tStations_LabelAngle_Col, label.getAngle());
			ih.bind(tStations_LabelVisible_Col, label.isVisible());

			long stationRowId = ih.execute();
			if (stationRowId < 0) {
				return null;
			}

			stationsRowsMap.put(station, stationRowId);
		}

		// TODO station aliases
		//
		// ContentValues aliasValues = new ContentValues();
		// aliasValues.put(StationAliasesTable.Columns.STATION_ID,
		// stationRowId);
		// aliasValues.put(StationAliasesTable.Columns.NAME, station.getName());
		// long nameRowId = db.insert(StationAliasesTable.NAME,
		// StationAliasesTable.Columns.NAME, aliasValues);
		// if (nameRowId < 0) {
		// return -1;
		// }
		//
		// stationValues.clear();
		// stationValues.put(StationsTable.Columns.NAME_ID, nameRowId);
		// db.update(StationsTable.NAME, stationValues,
		// StationsTable.Columns._ID
		// + " = " + stationRowId, null);

		return stationsRowsMap;
	}

	/**
	 * Inserts the {@link Line}s data into the given {@link SQLiteDatabase}.
	 * 
	 * @param db
	 * @param lines
	 * @param mapRowId
	 * @return the line-to-rowId map of the newly inserted rows, or null if an
	 *         error occurred
	 */
	private static Map<Line, Long> insertLines(SQLiteDatabase db,
			Set<Line> lines, long mapRowId) {

		InsertHelper ih = new InsertHelper(db, LinesTable.NAME);
		final int tLines_Name_Col = ih.getColumnIndex(LinesTable.Columns.NAME);
		final int tLines_MapId_Col = ih
				.getColumnIndex(LinesTable.Columns.MAP_ID);
		final int tLines_Color_Col = ih
				.getColumnIndex(LinesTable.Columns.COLOR);
		final int tLines_TrainDelay_Col = ih
				.getColumnIndex(LinesTable.Columns.TRAIN_DELAY);

		Map<Line, Long> linesRowsMap = new HashMap<Line, Long>(lines.size(), 1);

		for (Line line : lines) {

			ih.prepareForInsert();

			ih.bind(tLines_MapId_Col, mapRowId);
			ih.bind(tLines_Name_Col, line.getName());
			ih.bind(tLines_Color_Col, line.getColor());
			ih.bind(tLines_TrainDelay_Col, line.getTrainDelay());

			long lineRowId = ih.execute();
			if (lineRowId < 0) {
				return null;
			}

			linesRowsMap.put(line, lineRowId);
		}

		return linesRowsMap;
	}

	/**
	 * Inserts the {@link Transfer}s data into the given {@link SQLiteDatabase}.
	 * 
	 * @param db
	 * @param transfers
	 * @param stationsRowsMap
	 * @return true if all of the transfers were inserted successfully, false
	 *         otherwise
	 */
	private static boolean insertTransfers(SQLiteDatabase db,
			Set<Transfer> transfers, Map<Station, Long> stationsRowsMap) {

		InsertHelper ih = new InsertHelper(db, TransfersTable.NAME);
		final int tTransfers_Time_Col = ih
				.getColumnIndex(TransfersTable.Columns.TIME);
		final int tTransfers_FromStationId_Col = ih
				.getColumnIndex(TransfersTable.Columns.FROM_STATION_ID);
		final int tTransfers_ToStationId_Col = ih
				.getColumnIndex(TransfersTable.Columns.TO_STATION_ID);

		for (Transfer transfer : transfers) {

			Long fromStationRowId = stationsRowsMap.get(transfer
					.getFromStation());
			Long toStationRowId = stationsRowsMap.get(transfer.getToStation());
			if (fromStationRowId == null || toStationRowId == null) {
				throw new RuntimeException(
						"abnormal: null retrieved from stationsRowsMap");
			}

			ih.prepareForInsert();

			ih.bind(tTransfers_FromStationId_Col, fromStationRowId);
			ih.bind(tTransfers_ToStationId_Col, toStationRowId);
			ih.bind(tTransfers_Time_Col, transfer.getTime());

			if (ih.execute() < 0) {
				return false;
			}

		}

		return true;
	}

	/**
	 * Inserts the {@link Tunnel}s data into the given {@link SQLiteDatabase}.
	 * 
	 * @param db
	 * @param tunnels
	 * @param stationsRowsMap
	 * @param linesRowsMap
	 * @return true if all of the tunnels were inserted successfully, false
	 *         otherwise
	 */
	private static boolean insertTunnels(SQLiteDatabase db,
			Set<Tunnel> tunnels, Map<Station, Long> stationsRowsMap,
			Map<Line, Long> linesRowsMap) {

		InsertHelper ih = new InsertHelper(db, TunnelsTable.NAME);
		final int tTunnels_Time_Col = ih
				.getColumnIndex(TunnelsTable.Columns.TIME);
		final int tTunnels_FromStationId_Col = ih
				.getColumnIndex(TunnelsTable.Columns.FROM_STATION_ID);
		final int tTunnels_ToStationId_Col = ih
				.getColumnIndex(TunnelsTable.Columns.TO_STATION_ID);
		final int tTunnels_LineId_Col = ih
				.getColumnIndex(TunnelsTable.Columns.LINE_ID);
		final int tTunnels_Smooth_Col = ih
				.getColumnIndex(TunnelsTable.Columns.SMOOTH);

		for (Tunnel tunnel : tunnels) {

			Long fromStationRowId = stationsRowsMap
					.get(tunnel.getFromStation());
			Long toStationRowId = stationsRowsMap.get(tunnel.getToStation());
			if (fromStationRowId == null || toStationRowId == null) {
				throw new RuntimeException(
						"abnormal: null retrieved from stationsRowsMap");
			}
			Long lineRowId = linesRowsMap.get(tunnel.getLine());
			if (lineRowId == null) {
				throw new RuntimeException(
						"abnormal: null retrieved from linesRowsMap");
			}

			ih.prepareForInsert();

			ih.bind(tTunnels_FromStationId_Col, fromStationRowId);
			ih.bind(tTunnels_ToStationId_Col, toStationRowId);
			ih.bind(tTunnels_Time_Col, tunnel.getTime());
			ih.bind(tTunnels_LineId_Col, lineRowId);
			ih.bind(tTunnels_Smooth_Col, tunnel.isSmooth());

			if (ih.execute() < 0) {
				return false;
			}

		}

		return true;
	}

}
